﻿using System;
using System.Collections.Generic;
using System.Linq;

using Microsoft.WowAddonStudio.Lua.AST;
using Microsoft.WowAddonStudio.Lua.Parser;
using Microsoft.WowAddonStudio.Lua.Support;

namespace Microsoft.WowAddonStudio.Lua.Model
{
    /// <summary>
    /// The qualified name of a Lua declaration. This type is immutable.
    /// </summary>
    public sealed class QualifiedName : Node, IEquatable<QualifiedName>
    {
        private const char separator = '.';
        private const char methodSeparator = ':';

        private readonly LinkedList<Identifier> identifiers;
        
        /// <summary>
        /// Initializes a new instance of the <see cref="QualifiedName"/> class.
        /// </summary>
        /// <param name="identifier">The identifier.</param>
        public QualifiedName(Identifier identifier) : base(identifier.Location)
        {
            if (identifier == null)
                throw new ArgumentNullException("identifier");

            this.identifiers = new LinkedList<Identifier>();
            this.identifiers.AddLast(identifier);
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="QualifiedName"/> class.
        /// </summary>
        /// <param name="identifiers">The identifiers.</param>
        public QualifiedName(IEnumerable<Identifier> identifiers)
            : base(LexLocation.Merge(identifiers.First().Location, identifiers.Last().Location))
        {
            if (identifiers == null)
                throw new ArgumentNullException("identifiers");
            if (identifiers.Count() == 0)
                throw new ArgumentException("Identifier collection contains no items.");

            this.identifiers = new LinkedList<Identifier>(identifiers);
        }

        /// <summary>
        /// Further qualifies a name with a method name.
        /// </summary>
        /// <param name="identifier">The identifier of the method.</param>
        /// <returns>An instance of the <see cref="QualifiedName"/> class.</returns>
        public QualifiedName QualifyMethod(Identifier identifier)
        {
            return this.Qualify(identifier, true);
        }

        /// <summary>
        /// Further qualifies a name.
        /// </summary>
        /// <param name="identifier">The identifier to qualify with.</param>
        /// <returns>An instance of the <see cref="QualifiedName"/> class.</returns>
        public QualifiedName Qualify(Identifier identifier)
        {
            return this.Qualify(identifier, false);
        }

        private QualifiedName Qualify(Identifier identifier, bool isMethod)
        {
            if (identifier == null)
                throw new ArgumentNullException("identifier");
            if (this.IsMethod)
                throw new InvalidOperationException("Cannot further qualify method name.");

            return new QualifiedName(this.identifiers.Concat(new[]
                                                             {
                                                                 identifier
                                                             }))
                   {
                       IsMethod = isMethod
                   };
        }

        /// <summary>
        /// Unqualifies the name by removing the first or the last qualifier.
        /// </summary>
        /// <param name="last">If true, the last qualifier is removed; the first is removed otherwise.</param>
        /// <returns></returns>
        public QualifiedName Unqualify(bool last)
        {
            if (!IsQualified)
                throw new InvalidOperationException("Name is not qualified.");

            if (last)
                return new QualifiedName(identifiers.Take(identifiers.Count() - 1));

            return new QualifiedName(identifiers.Skip(1))
                   {
                       IsMethod = this.IsMethod
                   };

        }

        /// <summary>
        /// Unqualifies the name by returning a <see cref="QualifiedName"/> without the <see cref="FirstIdentifier"/>.
        /// </summary>
        /// <returns></returns>
        public QualifiedName Unqualify()
        {
            return this.Unqualify(false);
        }

        /// <summary>
        /// Gets a value indicating whether the <see cref="QualifiedName"/> qualifies a method.
        /// </summary>
        public bool IsMethod { get; private set; }

        /// <summary>
        /// Gets a value indicating whether the qualified name is empty.
        /// </summary>
        public bool IsQualified
        {
            get { return identifiers.Count() > 1; }
        }

        /// <summary>
        /// Gets the first qualifier in the qualified name.
        /// </summary>
        public Identifier FirstIdentifier
        {
            get { return identifiers.First != null ? identifiers.First.Value : null; }
        }


        /// <summary>
        /// Gets the name without the qualifications.
        /// </summary>
        public Identifier UnqualifiedIdentifier
        {
            get { return identifiers.Last(); }
        }

        /// <summary>
        /// Gets the name of the method without the qualifications.
        /// </summary>
        public Identifier MethodIdentifier
        {
            get
            {
                if (!IsMethod)
                    throw new InvalidOperationException("Qualified name does not qualify a method.");

                return this.UnqualifiedIdentifier;
            }
        }
        /// <summary>
        /// Performs an implicit conversion from <see cref="System.String"/> to <see cref="QualifiedName"/>.
        /// </summary>
        /// <param name="qualifiedName">The dotted name.</param>
        /// <returns>The result of the conversion.</returns>
        public static implicit operator QualifiedName(string qualifiedName)
        {
            if (qualifiedName == null)
                return null;

            string[] parts = qualifiedName.Split(methodSeparator);
            string[] identifierNames = parts[0].Split(separator);

            // If qualified name has more than one part (split at method separator), qualify method also
            if (parts.Length > 1)
                return new QualifiedName(identifierNames.Select<string, Identifier>(identifierName => identifierName)).QualifyMethod(parts[1]);

            // Return qualified name without method qualification
            return new QualifiedName(identifierNames.Select<string, Identifier>(identifierName => identifierName));
        }

        /// <summary>
        /// Implements the operator ==.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator ==(QualifiedName left, QualifiedName right)
        {
            if (Object.ReferenceEquals(left, right))
                return true;
            if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
                return false;

            return (left.ToString() == right.ToString());
        }

        /// <summary>
        /// Implements the operator !=.
        /// </summary>
        /// <param name="left">The left.</param>
        /// <param name="right">The right.</param>
        /// <returns>The result of the operator.</returns>
        public static bool operator !=(QualifiedName left, QualifiedName right)
        {
            return !(left == right);
        }

        /// <summary>
        /// Determines whether the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <param name="obj">The <see cref="T:System.Object"/> to compare with the current <see cref="T:System.Object"/>.</param>
        /// <returns>
        /// true if the specified <see cref="T:System.Object"/> is equal to the current <see cref="T:System.Object"/>; otherwise, false.
        /// </returns>
        /// <exception cref="T:System.NullReferenceException">
        /// The <paramref name="obj"/> parameter is null.
        /// </exception>
        public override bool Equals(object obj)
        {
            return (this == obj as QualifiedName);
        }

        /// <summary>
        /// Serves as a hash function for a particular type.
        /// </summary>
        /// <returns>
        /// A hash code for the current <see cref="T:System.Object"/>.
        /// </returns>
        public override int GetHashCode()
        {
            return this.ToString().GetHashCode();
        }

        /// <summary>
        /// Indicates whether the current object is equal to another object of the same type.
        /// </summary>
        /// <param name="other">An object to compare with this object.</param>
        /// <returns>
        /// true if the current object is equal to the <paramref name="other"/> parameter; otherwise, false.
        /// </returns>
        bool IEquatable<QualifiedName>.Equals(QualifiedName other)
        {
            return (this == other);
        }

        /// <summary>
        /// Returns a <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </summary>
        /// <returns>
        /// A <see cref="T:System.String"/> that represents the current <see cref="T:System.Object"/>.
        /// </returns>
        public override string ToString()
        {
            if (this.IsQualified && this.IsMethod)
                return String.Concat(this.Unqualify(true), methodSeparator, MethodIdentifier);

            return identifiers.Select(identifier => identifier.Name).Concatenate(separator);
        }
    }
}
